{
"cells": [
{
"cell_type": "markdown",
"id": "1c075cc7",
"metadata": {},
"source": [
"# Usage as a library\n",
"\n",
"This guide introduces basic concepts needed for using PyQBench as a library from a Python\n",
"script. In particular, we will cover the following topics:\n",
"\n",
"- Defining experiment using circuit components.\n",
"- Assembling circuits needed for given benchmarking scheme.\n",
"- Running experiment on simulator or hardware.\n",
"- Obtaining empirical probability of successful discrimination between measurements.\n",
"\n",
"## Setting the stage\n",
"\n",
"Before we start, make sure you installed PyQBench (see installation for \n",
"detailed instructions).\n",
"Optionally, you might want to install matplotlib library for plotting the final results.\n",
"\n",
"In this guide we won't repeat mathematical foundations needed for understanding measurement\n",
"discrimination experiments. We'll only restrict ourselves to the details necessary for running \n",
"experiments using PyQBench. However, we encourage you to check out {ref}`section/math` to\n",
"get a better grasp of the entities and concepts discussed here.\n",
"\n",
"## What do we need?\n",
"\n",
"On the conceptual level, every discrimination experiment performed in PyQBench needs the following:\n",
"\n",
"- Discriminator, i.e. optimal initial state for the circuit.\n",
"- Unitary $U^\\dagger$ defining alternative measurement to be discriminated from the measurement\n",
" in Z-basis.\n",
"- $V_0^\\dagger$ and $V_1^\\dagger$, positive and negative parts of optimal \n",
" measurement from Holevo-Helstrom theorem.\n",
"\n",
"Currently available quantum computers typically cannot start execution from an arbitrary state. \n",
"Instead, we have to prepare it ourselves. Hence, to execute experiment using postselection \n",
"scheme, we need the following operations implemented as Qiskit Instructions:\n",
"\n",
"- An instruction taking $|00\\rangle$ to the desired discriminator.\n",
"- Instructions implementing $U^\\dagger$, $V_0^\\dagger$ and $V_1^\\dagger$.\n",
"\n",
"Task of implementing needed instruction is trivial when we know the decomposition of our \n",
"unitaries into sequences of gates. If we only know the unitary matrices, we can either decompose \n",
"them by hand, or try using one of the available transpilers.\n",
"\n",
"For the direct-sum experiment we don't use $V_0^\\dagger$ and $V_1^\\dagger$ separately. Instead, \n",
"we need a two-qubit gate $V_0^\\dagger \\oplus V_1^\\dagger$.\n",
"\n",
"## Our toy model\n",
"\n",
"In our example, we will use $U = H$ (the Hadamard gate). To keep us focused on the implementation\n",
"in PyQBench and not delve too deep into mathematical explanation, we simply provide explicit \n",
"formulas for discriminator $|\\Psi_0\\rangle$ and $V_0$ and $V_1$, leaving the \n",
"calculations to the interested reader.\n",
"\n",
"The explicit formula for discriminator in our toy model reads:\n",
"\n",
"$$\n",
"| \\Psi_0 \\rangle = \\frac{1}{\\sqrt{2}} (| 00 \\rangle + | 11 \\rangle),\n",
"$$ \n",
"\n",
"with corresponding parts of optimal measurement being equal to \n",
"\n",
"$$\n",
"V_0 = \\begin{bmatrix}\n",
"\\alpha & -\\beta \\\\\n",
"\\beta & \\alpha\n",
"\\end{bmatrix}\n",
"\\quad\n",
"V_1 = \\begin{bmatrix}\n",
"-\\beta & \\alpha \\\\\n",
"\\alpha & \\beta\n",
"\\end{bmatrix}\n",
"$$\n",
"\n",
"where\n",
"\n",
"$$\n",
"\\alpha = \\frac{\\sqrt{2 - \\sqrt{2}}}{2} = \\cos\\left(\\frac{3}{8}\\pi\\right)\n",
"$$\n",
"\n",
"$$\n",
"\\beta = \\frac{\\sqrt{2 + \\sqrt{2}}}{2} = \\sin\\left(\\frac{3}{8}\\pi\\right)\n",
"$$\n",
"\n",
"For completeness, here's how the direct sum $V_0 \\oplus V_1$ looks like\n",
"\n",
"$$\n",
"V_0 \\oplus V_1 = \\begin{bmatrix}\n",
"V_0 & 0 \\\\\n",
"0 & V_1\n",
"\\end{bmatrix} = \\begin{bmatrix}\n",
"\\alpha & -\\beta & 0 & 0 \\\\\n",
"\\beta & \\alpha & 0 & 0 \\\\\n",
"0 & 0 & -\\beta & \\alpha \\\\\n",
"0 & 0 & \\alpha & \\beta\n",
"\\end{bmatrix}\n",
"$$\n",
"\n",
"As a next step, we need decompose our matrices into actual sequences of gates.\n",
"\n",
"## Decomposing circuit components into gates\n",
"\n",
"We are lucky, because our discriminator is just a Bell state. Thus, the circuit taking \n",
"$|00\\rangle$ to $|\\Psi_0 \\rangle$ is well known, and comprises Hadamard gate \n",
"followed by CNOT gate on both qubits.\n",
"\n",
"< PLACEHOLDER FOR CIRCUIT >\n",
"\n",
"For $V_0$ and $V_1$ observe that $V_0 = \\operatorname{RY} \\left( \\frac{3}{4} \\pi \\right)$, where \n",
"$\\operatorname{RY}$ is just standard rotation around the $Y$ axis\n",
"\n",
"$$\n",
"\\operatorname{RY}(\\theta) = \\begin{bmatrix}\n",
"\\cos \\frac{\\theta}{2} & -\\sin \\frac{\\theta}{2} \\\\\n",
"\\sin \\frac{\\theta}{2} & \\cos \\frac{\\theta}{2}\n",
"\\end{bmatrix}\n",
"$$\n",
"\n",
"To obtain $V_1$, we need only to swap the columns, which is equivalent to following $V_0$ by $X$ \n",
"matrix. Finally, remembering that we need to take Hermitian conjugates for our actual circuits,\n",
"we obtain the following decompositions\n",
"\n",
"$$\n",
"V_0^\\dagger = \\operatorname{RY} \\left( \\frac{3}{4} \\pi \\right)^\\dagger = \\operatorname{RY} \\left\n",
"( -\\frac{3}{4} \\pi \\right)\n",
"$$\n",
"\n",
"$$\n",
"V_1^\\dagger = \\left(\\operatorname{RY} \\left( \\frac{3}{4} \\pi \\right) \\cdot X\\right)^\\dagger = \n",
"X \\cdot \\operatorname{RY} \\left ( -\\frac{3}{4} \\pi \\right)\n",
"$$\n",
"\n",
"Recall that to perform an experiment using postselection scheme we need four circuits. One of them \n",
"(realizing $(U, V_0)$ alternative) looks like this.\n",
"\n",
"{#imgattr width=\"70%\"}\n",
"\n",
"Other circuits can be created analogously by using identity instead of $U$ and/or $V_1^\\dagger$ \n",
"instead of $V_0^\\dagger$. However, you don't need to memorize how the circuits look like, because\n",
"qbench will construct them for you."
]
},
{
"cell_type": "markdown",
"id": "21fc7fcd",
"metadata": {},
"source": [
"## Defining needed instructions using Qiskit\n",
"\n",
"We will start our code with the needed imports. Aside standard stuff like \n",
"numpy, we need to be able to define quantum circuits and a simulator to run \n",
"them."
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "29821678",
"metadata": {},
"outputs": [],
"source": [
"from qiskit import QuantumCircuit, Aer\n",
"import numpy as np"
]
},
{
"cell_type": "markdown",
"id": "c7200265",
"metadata": {},
"source": [
"Next we import needed functions from PyQBench. For our first example we'll \n",
"need two functions."
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "2ab223c7",
"metadata": {},
"outputs": [],
"source": [
"from qbench.schemes.postselection import benchmark_using_postselection\n",
"from qbench.schemes.direct_sum import benchmark_using_direct_sum"
]
},
{
"cell_type": "markdown",
"id": "8b1b9699",
"metadata": {},
"source": [
"The first one, {meth}`~qbench.schemes.postselection.benchmark_using_postselection` performs \n",
"the whole benchmarking process using postselection scheme. In particular, it \n",
"assembles the needed circuits, runs them using specified backend and \n",
"interprets measurement histograms in terms of discrimination probability. \n",
"Similarly, the {meth}`~qbench.schemes.direct_sum.benchmark_using_direct_sum` \n",
"does the same but with \"direct sum\" scheme.\n",
"\n",
"To run any of these functions, we need to define components that we \n",
"discussed in previous sections. Its perhaps best to do this by defining \n",
"separate function for each component. The important thing to remember is \n",
"that we need to create Qiskit instructions, not circuits. We can \n",
"conveniently do so by constructing circuit acting on qubits 0 and 1 and then \n",
"converting them using [to_instruction()](https://qiskit.org/documentation/stubs/qiskit.circuit.QuantumCircuit.to_instruction.html)` method.\n"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "505e63c7",
"metadata": {},
"outputs": [],
"source": [
"def state_prep():\n",
" circuit = QuantumCircuit(2)\n",
" circuit.h(0)\n",
" circuit.cnot(0, 1)\n",
" return circuit.to_instruction()\n",
"\n",
"\n",
"def u_dag():\n",
" circuit = QuantumCircuit(1)\n",
" circuit.h(0)\n",
" return circuit.to_instruction()\n",
"\n",
"\n",
"def v0_dag():\n",
" circuit = QuantumCircuit(1)\n",
" circuit.ry(-np.pi * 3 / 4, 0)\n",
" return circuit.to_instruction()\n",
"\n",
"\n",
"def v1_dag():\n",
" circuit = QuantumCircuit(1)\n",
" circuit.ry(-np.pi * 3 / 4, 0)\n",
" circuit.x(0)\n",
" return circuit.to_instruction()\n",
"\n",
"\n",
"def v0_v1_direct_sum_dag():\n",
" circuit = QuantumCircuit(2)\n",
" circuit.ry(-np.pi * 3 / 4, 0)\n",
" circuit.cnot(0, 1)\n",
" return circuit.to_instruction()"
]
},
{
"cell_type": "markdown",
"id": "f7cfbcd7",
"metadata": {},
"source": [
":::{note}\n",
"You may wonder why we only define circuits on qubits 0 and 1, when we might \n",
"want to run the benchmarks for other qubits as well? It turns out that it \n",
"doesn't matter. In Qiskit, circuit converted to Instruction behaves just \n",
"like a gate. During the assembly stage, PyQBench will use those \n",
"instructions on correct qubits.\n",
":::\n",
"\n",
"## Running simulations in the simplest scenario\n",
"\n",
"Lastly, before launching our simulations, we need to construct simulator \n",
"they will run on. For the purpose of this example, we'll start with basic \n",
"[Qiskit Aer simulator](https://github.com/Qiskit/qiskit-aer)."
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "e34964ef",
"metadata": {},
"outputs": [],
"source": [
"simulator = Aer.get_backend(\"aer_simulator\")"
]
},
{
"cell_type": "markdown",
"id": "38253418",
"metadata": {},
"source": [
"Now running the simulation is as simple as invoking functions imported from \n",
"`qbench` package."
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "4d2c3703",
"metadata": {},
"outputs": [],
"source": [
"postselection_result = benchmark_using_postselection(\n",
" backend=simulator,\n",
" target=0,\n",
" ancilla=1,\n",
" state_preparation=state_prep(),\n",
" u_dag=u_dag(),\n",
" v0_dag=v0_dag(),\n",
" v1_dag=v1_dag(),\n",
" num_shots_per_measurement=10000,\n",
")"
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "aa590bef",
"metadata": {},
"outputs": [],
"source": [
"direct_sum_result = benchmark_using_direct_sum(\n",
" backend=simulator,\n",
" target=1,\n",
" ancilla=2,\n",
" state_preparation=state_prep(),\n",
" u_dag=u_dag(),\n",
" v0_v1_direct_sum_dag=v0_v1_direct_sum_dag(),\n",
" num_shots_per_measurement=10000,\n",
")"
]
},
{
"cell_type": "code",
"execution_count": 12,
"id": "3a5c109f",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Real p_succ = 0.8535533905932737\n",
"Postselection: p_succ = 0.8542241847738258, abs. error = -0.0006707941805520479\n",
"Direct sum: p_succ = 0.8536, abs. error = -4.6609406726294544e-05\n"
]
}
],
"source": [
"p_succ = (2 + np.sqrt(2)) / 4\n",
"print(f\"Real p_succ = {p_succ}\")\n",
"print(\n",
" f\"Postselection: p_succ = {postselection_result}, abs. error = {p_succ - postselection_result}\"\n",
")\n",
"print(f\"Direct sum: p_succ = {direct_sum_result}, abs. error = {p_succ - direct_sum_result}\")"
]
},
{
"cell_type": "markdown",
"id": "2dad87c5",
"metadata": {},
"source": [
"## Gaining more control over the benchmarking process\n",
"In the example presented above we used functions that automate the whole process - from the circuit assembly, through running the simulations to interpreting the results. But what if we want more control over some parts of this process? Maybe we want play around with some models? Or maybe we want to run the same experiment on multiple backends and we don't want them to be assembled over and over again?\n",
"\n",
"From our (i.e. the developers of PyQBench) perspective, one possibility would be to add more and more parameters to `benchmark_using_xyz` functions, but this approach clearly is not very scalable. Also, there is no way we can anticipate all the possible use cases!\n",
"\n",
"We decided on another approach. PyQBench provides functions performing:\n",
"\n",
"- assembly of circuits needed for experiment, provided the components discussed above\n",
"- interpretation of the obtained measurements\n",
"\n",
"When using these functions, you need to take care of running circuits on some backend yourself, but it gives you unlimited power over the whole process. The difference between the two approaches is illustrated on the diagrams below."
]
},
{
"cell_type": "code",
"execution_count": 29,
"id": "7f7c708c",
"metadata": {
"tags": [
"remove-cell"
]
},
"outputs": [],
"source": [
"import iplantuml"
]
},
{
"cell_type": "code",
"execution_count": 27,
"id": "c114c56c",
"metadata": {
"tags": [
"remove-input",
"remove-stdout"
]
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Writing output for /home/dexter/Projects/iitis/PyQBench/docs/source/notebooks/cee7e7f7-817f-4e86-a60b-d0d1bf48419f.uml to cee7e7f7-817f-4e86-a60b-d0d1bf48419f.svg\n"
]
},
{
"data": {
"image/svg+xml": [
""
],
"text/plain": [
""
]
},
"execution_count": 27,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"%%plantuml\n",
"@startuml\n",
"!theme mars\n",
"title Simplified benchmarking\n",
"actor User\n",
"participant PyQBench\n",
"boundary Backend\n",
"\n",
"User --> PyQBench: passes circuit components,\\nbackend and number of shots\n",
"PyQBench --> PyQBench: assembles the circuits\n",
"PyQBench --> Backend: submits circuits to be executed\n",
"Backend --> PyQBench: returns measurements\n",
"PyQBench --> PyQBench: compute probability\n",
"PyQBench --> User: return probability of success\n",
"@enduml\n"
]
},
{
"cell_type": "code",
"execution_count": 28,
"id": "a255d808",
"metadata": {
"tags": [
"remove-input",
"remove-stdout"
]
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Writing output for /home/dexter/Projects/iitis/PyQBench/docs/source/notebooks/3dee3601-2949-4408-a48e-8a3ab89072da.uml to 3dee3601-2949-4408-a48e-8a3ab89072da.svg\n"
]
},
{
"data": {
"image/svg+xml": [
""
],
"text/plain": [
""
]
},
"execution_count": 28,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"%%plantuml\n",
"@startuml\n",
"!theme mars\n",
"title Execution of circuits controlled by user\n",
"actor User\n",
"participant PyQBench\n",
"boundary Backend\n",
"\n",
"User --> PyQBench: passes circuit components and qubit indices\n",
"PyQBench --> User: returns assembled circuits\n",
"User --> Backend: submits circuits to be executed\n",
"Backend --> User: returns raw measurements\n",
"User --> PyQBench: passess measurements\n",
"PyQBench --> User: returns computed probability\n",
"@enduml\n"
]
},
{
"cell_type": "markdown",
"id": "1cb4666a",
"metadata": {},
"source": [
"### Assembling circuits\n",
"Let us focus only on the postselection case, as the direct sum case is analogous. First, we need to import two more functions from PyQBench."
]
},
{
"cell_type": "code",
"execution_count": 30,
"id": "b4c6daf9",
"metadata": {},
"outputs": [],
"source": [
"from qbench.schemes.postselection import (\n",
" assemble_postselection_circuits,\n",
" compute_probabilities_from_postselection_measurements,\n",
")"
]
},
{
"cell_type": "code",
"execution_count": 33,
"id": "ba50b4a3",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'id_v0': ,\n",
" 'id_v1': ,\n",
" 'u_v0': ,\n",
" 'u_v1': }"
]
},
"execution_count": 33,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"circuits = assemble_postselection_circuits(\n",
" target=0,\n",
" ancilla=1,\n",
" state_preparation=state_prep(),\n",
" u_dag=u_dag(),\n",
" v0_dag=v0_dag(),\n",
" v1_dag=v1_dag(),\n",
")\n",
"\n",
"circuits"
]
},
{
"cell_type": "markdown",
"id": "65964ee3",
"metadata": {},
"source": [
"Remember how the postselection requires 4 circuits? The `assemble_postselection_circuits` created all of them, nicely packed in a dictionary. Each informs if the circuit implements identity or alternative measurement on the target qubit, and which of $V_0$ and $V_1$ is used.\n",
"\n",
"Now we only need to run the circuits.\n",
"\n",
"### Running the circuits\n",
"\n",
"To make things more interesting, we will run a noisy and noiseless simulation of our circuits. We will use the same backend as before, and our noise model will only comprise readout errors on both qubits."
]
},
{
"cell_type": "code",
"execution_count": 85,
"id": "39d0ac4f",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
""
]
},
"execution_count": 85,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"from qiskit.providers.aer import noise\n",
"\n",
"error = noise.ReadoutError([[0.75, 0.25], [0.8, 0.2]])\n",
"\n",
"noise_model = noise.NoiseModel()\n",
"noise_model.add_readout_error(error, [0])\n",
"noise_model.add_readout_error(error, [1])\n",
"\n",
"noise_model"
]
},
{
"cell_type": "markdown",
"id": "01698833",
"metadata": {},
"source": [
"Once we have our noise model ready, we can execute our circuits with and without noise. To this end, we will use Qiskit's execute function. One caveat is that we have to keep track which measurements correspond to which circuit. We do so by fixing an ordering on the keys in `circuits` dictionary.\n",
"\n",
":::{note}\n",
"Actually, the order of keys in the dictionary **is** fixed in modern versions of Python (i.e. iterating twice over the same dictionary without modifying it will always yield the same results. However, we use explicit ordering to make the example more accessible, especially for the readers less experienced in Python.\n",
":::"
]
},
{
"cell_type": "code",
"execution_count": 82,
"id": "c265e41d",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Noisless counts: [{'11': 734, '01': 4231, '00': 724, '10': 4311}, {'01': 716, '10': 737, '00': 4238, '11': 4309}, {'01': 749, '10': 697, '00': 4361, '11': 4193}, {'01': 4197, '11': 742, '00': 736, '10': 4325}]\n",
"Noisy counts: [{'11': 464, '01': 1749, '10': 1741, '00': 6046}, {'11': 493, '10': 1729, '00': 5971, '01': 1807}, {'11': 524, '00': 5965, '10': 1734, '01': 1777}, {'11': 472, '01': 1700, '10': 1749, '00': 6079}]\n"
]
}
],
"source": [
"from qiskit import execute\n",
"\n",
"keys_ordering = [\"id_v0\", \"id_v1\", \"u_v0\", \"u_v1\"]\n",
"all_circuits = [circuits[key] for key in keys_ordering]\n",
"\n",
"counts_noisy = (\n",
" execute(all_circuits, backend=simulator, noise_model=noise_model, shots=10000)\n",
" .result()\n",
" .get_counts()\n",
")\n",
"\n",
"counts_noiseless = execute(all_circuits, backend=simulator, shots=10000).result().get_counts()\n",
"\n",
"\n",
"print(f\"Noisless counts: {counts_noiseless}\")\n",
"print(f\"Noisy counts: {counts_noisy}\")"
]
},
{
"cell_type": "markdown",
"id": "86300bc3",
"metadata": {},
"source": [
"### Computing probabilities\n",
"The only thing left is to compute the success probabilities. We do so by passing bitstring counts to `compute_probabilities_from_postselection_measurements` function."
]
},
{
"cell_type": "code",
"execution_count": 88,
"id": "633ca70a",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"0.856421162176477\n"
]
}
],
"source": [
"prob_succ_noiseless = compute_probabilities_from_postselection_measurements(\n",
" id_v0_counts=counts_noiseless[0],\n",
" id_v1_counts=counts_noiseless[1],\n",
" u_v0_counts=counts_noiseless[2],\n",
" u_v1_counts=counts_noiseless[3],\n",
")\n",
"\n",
"print(prob_succ_noiseless)"
]
},
{
"cell_type": "code",
"execution_count": 89,
"id": "36157e67",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"0.4988475737326284\n"
]
}
],
"source": [
"prob_succ_noisy = compute_probabilities_from_postselection_measurements(\n",
" id_v0_counts=counts_noisy[0],\n",
" id_v1_counts=counts_noisy[1],\n",
" u_v0_counts=counts_noisy[2],\n",
" u_v1_counts=counts_noisy[3],\n",
")\n",
"\n",
"print(prob_succ_noisy)"
]
},
{
"cell_type": "markdown",
"id": "814396c4",
"metadata": {},
"source": [
"As expected, noisy simulations gave us results that are further away from the exact ones.\n",
"\n",
"This concludes introduction to PyQBench library. If you are interested see additoinal usage examples in our examples directory."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "7e295d78",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"celltoolbar": "Edit Metadata",
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.9.15"
}
},
"nbformat": 4,
"nbformat_minor": 5
}